package org.farng.mp3;

import org.farng.mp3.filename.FilenameTag;
import org.farng.mp3.filename.FilenameTagBuilder;
import org.farng.mp3.id3.AbstractID3v2;
import org.farng.mp3.id3.AbstractID3v2Frame;
import org.farng.mp3.id3.ID3v1;
import org.farng.mp3.id3.ID3v1_1;
import org.farng.mp3.id3.ID3v2_2;
import org.farng.mp3.id3.ID3v2_3;
import org.farng.mp3.id3.ID3v2_4;
import org.farng.mp3.lyrics3.AbstractLyrics3;
import org.farng.mp3.lyrics3.Lyrics3v1;
import org.farng.mp3.lyrics3.Lyrics3v2;

import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;


/**
 * This class holds both ID3 tags, Lyrics3 tag, and MP3 header information. It
 * also contains methods that act against these tags or the MP3 file itself.
 *
 * @author Eric Farng
 * @version $Revision: 1.20 $
 */
public class MP3File {
    /** the ID3v2 tag that this file contains. */
    private AbstractID3v2 id3v2tag = null;

    /** the Lyrics3 tag that this file contains. */
    private AbstractLyrics3 lyrics3tag = null;

    /**
     * the mp3 file that this instance represents. This value can be null. This
     * value is also used for any methods that are called without a file
     * argument
     */
    private File mp3file;

    /** the ID3v2_4 tag that represents the parsed filename. */
    private FilenameTag filenameTag = null;

    /** the ID3v1 tag that this file contains. */
    private ID3v1 id3v1tag = null;

    /** value read from the MP3 Frame header */
    private boolean copyProtected;

    /** value read from the MP3 Frame header */
    private boolean home;

    /** value read from the MP3 Frame header */
    private boolean padding;

    /** value read from the MP3 Frame header */
    private boolean privacy;

    /** value read from the MP3 Frame header */
    private boolean protection;

    /** DOCUMENT ME! */
    private boolean variableBitRate = false;

    /** value read from the MP3 Frame header */
    private byte emphasis;

    /** value read from the MP3 Frame header */
    private byte layer;

    /** value read from the MP3 Frame header */
    private byte mode;

    /** value read from the MP3 Frame header */
    private byte modeExtension;

    /** value read from the MP3 Frame header */
    private byte mpegVersion;

    /**
     * frequency determined from MP3 Version and frequency value read from the
     * MP3 Frame header
     */
    private double frequency;

    /** bitrate calculated from the frame MP3 Frame header */
    private int bitRate;

    /**
     * Creates a new empty MP3File object that is not associated with a
     * specific file.
     */
    public MP3File() {}

    public MP3File(MP3File copyObject) {
        //      no copy constructor in super class
        this.copyProtected   = copyObject.copyProtected;
        this.home            = copyObject.home;
        this.padding         = copyObject.padding;
        this.privacy         = copyObject.privacy;
        this.protection      = copyObject.protection;
        this.variableBitRate = copyObject.variableBitRate;
        this.emphasis        = copyObject.emphasis;
        this.layer           = copyObject.layer;
        this.mode            = copyObject.mode;
        this.modeExtension   = copyObject.modeExtension;
        this.mpegVersion     = copyObject.mpegVersion;
        this.frequency       = copyObject.frequency;
        this.bitRate         = copyObject.bitRate;
        this.mp3file         = new File(copyObject.mp3file.getAbsolutePath());

        this.filenameTag = new FilenameTag(copyObject.filenameTag);
        this.id3v2tag    = (AbstractID3v2) TagUtility.copyObject(copyObject.id3v2tag);
        this.lyrics3tag  = (AbstractLyrics3) TagUtility.copyObject(copyObject.lyrics3tag);
        this.id3v1tag    = (ID3v1) TagUtility.copyObject(copyObject.id3v1tag);
    }

    /**
     * Creates a new MP3File object and parse the tag from the given filename.
     *
     * @param filename MP3 file
     *
     * @throws IOException on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public MP3File(String filename)
            throws IOException, TagException {
        this(new File(filename));
    }

    /**
     * Creates a new MP3File object and parse the tag from the given file
     * Object.
     *
     * @param file MP3 file
     *
     * @throws IOException on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public MP3File(File file)
            throws IOException, TagException {
        this.mp3file = file;

        //System.out.println("Reading : " + file.getAbsolutePath());
        RandomAccessFile newFile = new RandomAccessFile(file, "rw");

        try {
            id3v1tag = new ID3v1_1(newFile);
        } catch (TagNotFoundException ex) {}

        try {
            if (id3v1tag == null) {
                id3v1tag = new ID3v1(newFile);
            }
        } catch (TagNotFoundException ex) {}

        try {
            id3v2tag = new ID3v2_4(newFile);
        } catch (TagNotFoundException ex) {}

        try {
            if (id3v2tag == null) {
                id3v2tag = new ID3v2_3(newFile);
            }
        } catch (TagNotFoundException ex) {}

        try {
            if (id3v2tag == null) {
                id3v2tag = new ID3v2_2(newFile);
            }
        } catch (TagNotFoundException ex) {}

        try {
            lyrics3tag = new Lyrics3v2(newFile);
        } catch (TagNotFoundException ex) {}

        try {
            if (lyrics3tag == null) {
                lyrics3tag = new Lyrics3v1(newFile);
            }
        } catch (TagNotFoundException ex) {}

        newFile.close();

        try {
            filenameTag = FilenameTagBuilder.createFilenameTagFromMP3File(this);
        } catch (Exception ex) {
            throw new TagException("Unable to create FilenameTag", ex);
        }
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public int getBitRate() {
        return bitRate;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public boolean isCopyProtected() {
        return copyProtected;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public byte getEmphasis() {
        return emphasis;
    }

    /**
     * Sets the filename tag for this MP3 File. Refer to
     * <code>TagUtilities.parseFileName</code> and
     * <code>TagUtilities.createID3v2Tag</code> for more information about
     * parsing file names into <code>ID3v2_4</code> objects.
     *
     * @param filenameTag parsed <code>ID3v2_4</code> filename tag
     */
    public void setFilenameTag(FilenameTag filenameTag) {
        this.filenameTag = filenameTag;
    }

    /**
     * Sets the filename tag for this MP3 File. Refer to
     * <code>TagUtilities.parseFileName</code> and
     * <code>TagUtilities.createID3v2Tag</code> for more information about
     * parsing file names into <code>ID3v2_4</code> objects.
     *
     * @return parsed <code>ID3v2_4</code> filename tag
     */
    public FilenameTag getFilenameTag() {
        return this.filenameTag;
    }

    /**
     * Sets all four (id3v1, lyrics3, filename, id3v2) tags in this instance to
     * the <code>frame</code> argument if the tag exists. This method does not
     * use the options inside the <code>tagOptions</code> object.
     *
     * @param frame frame to set / replace in all four tags.
     *
     * @todo this method is very inefficient.
     */
    public void setFrameAcrossTags(AbstractID3v2Frame frame) {
        ID3v2_4 id3v1 = null;
        ID3v2_4 lyrics3 = null;

        if (this.id3v1tag != null) {
            id3v1 = new ID3v2_4(this.id3v1tag);
            id3v1.setFrame(frame);
            this.id3v1tag.overwrite(id3v1);
        }

        if (this.id3v2tag != null) {
            id3v2tag.setFrame(frame);
        }

        if (this.lyrics3tag != null) {
            lyrics3 = new ID3v2_4(this.lyrics3tag);
            lyrics3.setFrame(frame);
            this.lyrics3tag = new Lyrics3v2(lyrics3);
        }

        if (this.filenameTag != null) {
            filenameTag.setFrame(frame);
        }
    }

    /**
     * Gets the frames from all four (id3v1, lyrics3, filename, id3v2) mp3 tags
     * in this instance for each tag that exists. This method does not use the
     * options inside the <code>tagOptions</code> object.
     *
     * @param identifier ID3v2.4 Tag Frame Identifier.
     *
     * @return ArrayList of all instances of the desired frame. Each instance
     *         is returned as an <code>ID3v2_4Frame</code>. The nature of the
     *         code returns the array in a specific order, but this order is
     *         not guaranteed for future versions of this library.
     *
     * @todo this method is very inefficient.
     */
    public ArrayList getFrameAcrossTags(String identifier) {
        if ((identifier != null) && (identifier.length() > 0)) {
            ID3v2_4   id3v1 = null;
            ID3v2_4   lyrics3 = null;
            ArrayList list = new ArrayList();
            Iterator  iterator;

            if (this.id3v1tag != null) {
                id3v1 = new ID3v2_4(this.id3v1tag);

                if (id3v1.hasFrameOfType(identifier)) {
                    iterator = id3v1.getFrameOfType(identifier);

                    while (iterator.hasNext()) {
                        list.add(iterator.next());
                    }
                }
            }

            if (this.id3v2tag != null) {
                if (id3v2tag.hasFrameOfType(identifier)) {
                    iterator = id3v2tag.getFrameOfType(identifier);

                    while (iterator.hasNext()) {
                        list.add(iterator.next());
                    }
                }
            }

            if (this.lyrics3tag != null) {
                lyrics3 = new ID3v2_4(this.lyrics3tag);

                if (lyrics3.hasFrameOfType(identifier)) {
                    iterator = lyrics3.getFrameOfType(identifier);

                    while (iterator.hasNext()) {
                        list.add(iterator.next());
                    }
                }
            }

            if (this.filenameTag != null) {
                if (filenameTag.hasFrameOfType(identifier)) {
                    iterator = filenameTag.getFrameOfType(identifier);

                    while (iterator.hasNext()) {
                        list.add(iterator.next());
                    }
                }
            }

            return list;
        }

        return null;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public double getFrequency() {
        return frequency;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public boolean isHome() {
        return home;
    }

    /**
     * Sets the <code>ID3v1</code> tag for this object. A new
     * <code>ID3v1_1</code> object is created from the argument and then used
     * here.
     *
     * @param mp3tag Any MP3Tag object can be used and will be converted into a
     *        new ID3v1_1 object.
     */
    public void setID3v1Tag(AbstractMP3Tag mp3tag) {
        id3v1tag = new ID3v1_1(mp3tag);
    }

    /**
     * DOCUMENT ME!
     *
     * @param id3v1tag DOCUMENT ME!
     */
    public void setID3v1Tag(ID3v1 id3v1tag) {
        this.id3v1tag = id3v1tag;
    }

    /**
     * Returns the <code>ID3v1</code> tag for this object.
     *
     * @return the <code>ID3v1</code> tag for this object
     */
    public ID3v1 getID3v1Tag() {
        return id3v1tag;
    }

    /**
     * Sets the <code>ID3v2</code> tag for this object. A new
     * <code>ID3v2_4</code> object is created from the argument and then used
     * here.
     *
     * @param mp3tag Any MP3Tag object can be used and will be converted into a
     *        new ID3v2_4 object.
     */
    public void setID3v2Tag(AbstractMP3Tag mp3tag) {
        id3v2tag = new ID3v2_4(mp3tag);
    }

    /**
     * DOCUMENT ME!
     *
     * @param id3v2tag DOCUMENT ME!
     */
    public void setID3v2Tag(AbstractID3v2 id3v2tag) {
        this.id3v2tag = id3v2tag;
    }

    /**
     * Returns the <code>ID3v2</code> tag for this object.
     *
     * @return the <code>ID3v2</code> tag for this object
     */
    public AbstractID3v2 getID3v2Tag() {
        return id3v2tag;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public byte getLayer() {
        return layer;
    }

    /**
     * Sets the <code>Lyrics3</code> tag for this object. A new
     * <code>Lyrics3v2</code> object is created from the argument and then
     * used here.
     *
     * @param mp3tag Any MP3Tag object can be used and will be converted into a
     *        new Lyrics3v2 object.
     */
    public void setLyrics3Tag(AbstractMP3Tag mp3tag) {
        lyrics3tag = new Lyrics3v2(mp3tag);
    }

    /**
     * DOCUMENT ME!
     *
     * @param lyrics3tag DOCUMENT ME!
     */
    public void setLyrics3Tag(AbstractLyrics3 lyrics3tag) {
        this.lyrics3tag = lyrics3tag;
    }

    /**
     * Returns the <code>ID3v1</code> tag for this object.
     *
     * @return the <code>ID3v1</code> tag for this object
     */
    public AbstractLyrics3 getLyrics3Tag() {
        return lyrics3tag;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public byte getMode() {
        return mode;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public byte getModeExtension() {
        return modeExtension;
    }

    /**
     * Returns the byte position of the first MP3 Frame that this object refers
     * to. This is the first byte of music data and not the ID3 Tag Frame.
     *
     * @return the byte position of the first MP3 Frame
     *
     * @throws IOException on any I/O error
     * @throws FileNotFoundException if the file exists but is a directory
     *         rather than a regular file or cannot be opened for any other
     *         reason
     */
    public long getMp3StartByte()
                         throws IOException, FileNotFoundException {
        return getMp3StartByte(this.mp3file);
    }

    /**
     * Returns the byte position of the first MP3 Frame that the
     * <code>file</code> arguement refers to. This is the first byte of music
     * data and not the ID3 Tag Frame.
     *
     * @param file MP3 file to search
     *
     * @return the byte position of the first MP3 Frame
     *
     * @throws IOException on any I/O error
     * @throws FileNotFoundException if the file exists but is a directory
     *         rather than a regular file or cannot be opened for any other
     *         reason
     */
    public long getMp3StartByte(File file)
                         throws IOException, FileNotFoundException {
        RandomAccessFile rfile = null;
        long             startByte = 0;

        try {
            rfile = new RandomAccessFile(file, "r");
            seekMP3Frame(rfile);
            startByte = rfile.getFilePointer();
        } finally {
            if (rfile != null) {
                rfile.close();
            }
        }

        return startByte;
    }

    /**
     * DOCUMENT ME!
     *
     * @param mp3file DOCUMENT ME!
     */
    public void setMp3file(File mp3file) {
        this.mp3file = mp3file;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public File getMp3file() {
        return mp3file;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public byte getMpegVersion() {
        return mpegVersion;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public boolean isPadding() {
        return padding;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public boolean isPrivacy() {
        return privacy;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public boolean isProtection() {
        return protection;
    }

    /**
     * Returns true if there are any unsynchronized tags in this object. A
     * fragment is unsynchronized if it exists in two or more tags but is not
     * equal across all of them.
     *
     * @return true of any fragments are unsynchronized.
     *
     * @todo there might be a faster way to do this, other than calling
     *       <code>getUnsynchronizedFragments</code>
     */
    public boolean isUnsynchronized() {
        return getUnsynchronizedFragments().size() > 0;
    }

    /**
     * Returns a HashSet of unsynchronized fragments across all tags in this
     * object. A fragment is unsynchronized if it exists in two or more tags
     * but is not equal across all of them.
     *
     * @return a HashSet of unsynchronized fragments
     */
    public HashSet getUnsynchronizedFragments() {
        ID3v2_4            id3v1 = null;
        ID3v2_4            lyrics3 = null;
        ID3v2_4            filename = null;
        AbstractID3v2      id3v2 = null;
        ID3v2_4            total = new ID3v2_4(this.id3v2tag);
        AbstractID3v2Frame frame;
        String             identifier;

        HashSet            set = new HashSet();

        total.append(id3v1tag);
        total.append(lyrics3tag);
        total.append(filenameTag);
        total.append(id3v2tag);

        id3v1    = new ID3v2_4(this.id3v1tag);
        lyrics3  = new ID3v2_4(this.lyrics3tag);
        filename = new ID3v2_4(this.filenameTag);
        id3v2    = this.id3v2tag;

        Iterator iterator = total.iterator();

        while (iterator.hasNext()) {
            frame      = (AbstractID3v2Frame) iterator.next();
            identifier = frame.getIdentifier();

            if (id3v2 != null) {
                if (id3v2.hasFrame(identifier)) {
                    if (id3v2.getFrame(identifier).isSubsetOf(frame) == false) {
                        set.add(identifier);
                    }
                }
            }

            if (id3v1 != null) {
                if (id3v1.hasFrame(identifier)) {
                    if (id3v1.getFrame(identifier).isSubsetOf(frame) == false) {
                        set.add(identifier);
                    }
                }
            }

            if (lyrics3 != null) {
                if (lyrics3.hasFrame(identifier)) {
                    if (lyrics3.getFrame(identifier).isSubsetOf(frame) == false) {
                        set.add(identifier);
                    }
                }
            }

            if (filename != null) {
                if (filename.hasFrame(identifier)) {
                    if (filename.getFrame(identifier).isSubsetOf(frame) == false) {
                        set.add(identifier);
                    }
                }
            }
        }

        return set;
    }

    /**
     * DOCUMENT ME!
     *
     * @param variableBitRate DOCUMENT ME!
     */
    public void setVariableBitRate(boolean variableBitRate) {
        this.variableBitRate = variableBitRate;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public boolean isVariableBitRate() {
        return variableBitRate;
    }

    /**
     * Adjust the lenght of the ID3v2 padding at the beginning of the MP3 file
     * referred to in this object. The ID3v2 size will be calculated, then a
     * new file will be created with enough size to fit the <code>ID3v2</code>
     * tag in this object. The old file will be deleted, and the new file
     * renamed. All parameters will be taken from the <code>tagOptions</code>
     * object.
     *
     * @return DOCUMENT ME!
     *
     * @throws FileNotFoundException if the file exists but is a directory
     *         rather than a regular file or cannot be opened for any other
     *         reason
     * @throws IOException on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public boolean adjustID3v2Padding()
                               throws FileNotFoundException, IOException, TagException {
        return adjustID3v2Padding(TagOptionSingleton.getInstance().getId3v2PaddingSize(), TagOptionSingleton.getInstance().isId3v2PaddingWillShorten(), TagOptionSingleton.getInstance().isId3v2PaddingCopyTag(), this.mp3file);
    }

    /**
     * Adjust the length of the ID3v2 padding at the beginning of the MP3 file
     * this object refers to. The ID3v2 size will be calculated, then a new
     * file will be created with enough size to fit the <code>ID3v2</code>
     * tag. The old file will be deleted, and the new file renamed.
     *
     * @param paddingSize Initial padding size. This size is doubled until the
     *        ID3v2 tag will fit.
     * @param willShorten if the newly calculated padding size is less than the
     *        padding length of the file, then news the new shorter padding
     *        size if this is true.
     * @param copyID3v2Tag if true, write the <code>ID3v2</code> tag of this
     *        object into the file
     *
     * @return DOCUMENT ME!
     *
     * @throws FileNotFoundException if the file exists but is a directory
     *         rather than a regular file or cannot be opened for any other
     *         reason
     * @throws IOException on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public boolean adjustID3v2Padding(int paddingSize, boolean willShorten, boolean copyID3v2Tag)
                               throws FileNotFoundException, IOException, TagException {
        return adjustID3v2Padding(paddingSize, willShorten, copyID3v2Tag, this.mp3file);
    }

    /**
     * Adjust the length of the ID3v2 padding at the beginning of the MP3 file
     * this object refers to. The ID3v2 size will be calculated, then a new
     * file will be created with enough size to fit the <code>ID3v2</code>
     * tag. The old file will be deleted, and the new file renamed.
     *
     * @param paddingSize Initial padding size. This size is doubled until the
     *        ID3v2 tag will fit. A paddingSize of zero will create a padding
     *        length exactly equal to the tag size.
     * @param willShorten Shorten the padding size by halves if the ID3v2 tag
     *        will fit
     * @param copyID3v2Tag if true, write the <code>ID3v2</code> tag of this
     *        object into the file
     * @param file The file to adjust the padding length of
     *
     * @return DOCUMENT ME!
     *
     * @throws FileNotFoundException if the file exists but is a directory
     *         rather than a regular file or cannot be opened for any other
     *         reason
     * @throws IOException on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public boolean adjustID3v2Padding(int paddingSize, boolean willShorten, boolean copyID3v2Tag, File file)
                               throws FileNotFoundException, IOException, TagException {
        int              id3v2TagSize = 0;
        int              mp3start = (int) this.getMp3StartByte(file);
        FileOutputStream outStream = null;
        FileInputStream  inStream = null;
        File             backupFile = null;
        File             paddedFile = null;

        if (paddingSize < 0) {
            throw new TagException("Invalid paddingSize: " + paddingSize);
        }

        if (this.hasID3v2Tag()) {
            id3v2TagSize = this.getID3v2Tag().getSize();
        }

        if (paddingSize != 0) {
            // double padding size until it's large enough
            while (paddingSize < id3v2TagSize) {
                paddingSize *= TagOptionSingleton.getInstance().getId3v2PaddingMultiplier();
            }
        }

        if ((paddingSize < mp3start) && (willShorten == false)) {
            return false;
        }

        if (paddingSize == mp3start) {
            return false;
        }

        try {
            // we first copy everything to a new file, then replace the original
            paddedFile = File.createTempFile("temp", ".mp3", file.getParentFile());
            outStream  = new FileOutputStream(paddedFile);
            inStream   = new FileInputStream(file);

            byte[] buffer;

            if (copyID3v2Tag == true) {
                // paddingSize < mp3start && willshorten == false
                // was already checked for outside of the try block.
                if ((paddingSize < mp3start) && willShorten) {
                    // copy the current tag
                    buffer = new byte[paddingSize];
                    inStream.read(buffer, 0, buffer.length);
                    outStream.write(buffer, 0, buffer.length);
                    buffer = new byte[mp3start - paddingSize];

                    // skip the rest of the tag that didn't fit
                    inStream.read(buffer, 0, buffer.length);

                    // paddingSize > mp3start
                } else {
                    // copy the current tag
                    buffer = new byte[mp3start];
                    inStream.read(buffer, 0, buffer.length);
                    outStream.write(buffer, 0, buffer.length);

                    // add zeros for the rest of the padding
                    if ((paddingSize - mp3start) > 0) {
                        buffer = new byte[paddingSize - mp3start];
                        outStream.write(buffer, 0, buffer.length);
                    }
                }
            } else {
                buffer = new byte[paddingSize];

                // skip the tag
                inStream.skip(mp3start);

                // write zeros for the tag
                outStream.write(buffer, 0, buffer.length);
            }

            buffer = new byte[1024];

            int b = inStream.read(buffer, 0, buffer.length);

            while (b == 1024) {
                outStream.write(buffer, 0, buffer.length);
                b = inStream.read(buffer, 0, buffer.length);
            }

            if (b != -1) {
                outStream.write(buffer, 0, b);
            }

            backupFile = new File(file.getParentFile(), TagUtility.appendBeforeExtension(file.getName(), ".original"));
            TagUtility.copyFile(file, backupFile);

            if (backupFile.exists()) {
                backupFile.setLastModified(file.lastModified());
            } else {
                return false;
            }

            TagUtility.copyFile(paddedFile, file);

            return true;
        } finally {
            if (inStream != null) {
                inStream.getFD().sync();
                inStream.close();
            }

            if (outStream != null) {
                outStream.getFD().sync();
                outStream.close();
            }

            if ((backupFile != null) && (TagOptionSingleton.getInstance().isOriginalSavedAfterAdjustingID3v2Padding() == false)) {
                backupFile.delete();
            }

            if (paddedFile != null) {
                paddedFile.delete();
            }
        }
    }

    /**
     * DOCUMENT ME!
     *
     * @param mp3tag DOCUMENT ME!
     *
     * @throws FileNotFoundException DOCUMENT ME!
     * @throws IOException DOCUMENT ME!
     */
    public void delete(AbstractMP3Tag mp3tag)
                throws FileNotFoundException, IOException {
        mp3tag.delete(new RandomAccessFile(this.mp3file, "rw"));
    }

    /**
     * Returns true if this object contains an filename pseudo-tag
     *
     * @return true if this object contains an filename pseudo-tag
     */
    public boolean hasFilenameTag() {
        return (filenameTag != null);
    }

    /**
     * Returns true if this object contains an <code>Id3v1</code> tag
     *
     * @return true if this object contains an <code>Id3v1</code> tag
     */
    public boolean hasID3v1Tag() {
        return (id3v1tag != null);
    }

    /**
     * Returns true if this object contains an <code>Id3v2</code> tag
     *
     * @return true if this object contains an <code>Id3v2</code> tag
     */
    public boolean hasID3v2Tag() {
        return (id3v2tag != null);
    }

    /**
     * Returns true if this object contains an <code>Lyrics3</code> tag
     *
     * @return true if this object contains an <code>Lyrics3</code> tag
     */
    public boolean hasLyrics3Tag() {
        return (lyrics3tag != null);
    }

    /**
     * Saves the tags in this object to the file referred to by this object. It
     * will be saved as TagConstants.MP3_FILE_SAVE_WRITE
     *
     * @throws IOException on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public void save()
              throws IOException, TagException {
        save(this.mp3file, TagOptionSingleton.getInstance().getDefaultSaveMode());
    }

    /**
     * Saves the tags in this object to the file referred to by this object. It
     * will be saved as TagConstants.MP3_FILE_SAVE_WRITE
     *
     * @param saveMode write, overwrite, or append. Defined as
     *        <code>TagConstants.MP3_FILE_SAVE_WRITE
     *        TagConstants.MP3_FILE_SAVE_OVERWRITE
     *        TagConstants.MP3_FILE_SAVE_APPEND </code>
     *
     * @throws IOException on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public void save(int saveMode)
              throws IOException, TagException {
        save(this.mp3file, saveMode);
    }

    /**
     * Saves the tags in this object to the file argument. It will be saved as
     * TagConstants.MP3_FILE_SAVE_WRITE
     *
     * @param filename file to save the this object's tags to
     *
     * @throws IOException on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public void save(String filename)
              throws IOException, TagException {
        save(new File(filename), TagOptionSingleton.getInstance().getDefaultSaveMode());
    }

    /**
     * Saves the tags in this object to the file argument. It will be saved as
     * TagConstants.MP3_FILE_SAVE_WRITE
     *
     * @param filename file to save the this object's tags to
     * @param saveMode write, overwrite, or append. Defined as
     *        <code>TagConstants.MP3_FILE_SAVE_WRITE
     *        TagConstants.MP3_FILE_SAVE_OVERWRITE
     *        TagConstants.MP3_FILE_SAVE_APPEND </code>
     *
     * @throws IOException on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public void save(String filename, int saveMode)
              throws IOException, TagException {
        save(new File(filename), saveMode);
    }

    /**
     * Saves the tags in this object to the file argument. It will be saved as
     * TagConstants.MP3_FILE_SAVE_WRITE
     *
     * @param file file to save the this object's tags to
     * @param saveMode write, overwrite, or append. Defined as
     *        <code>TagConstants.MP3_FILE_SAVE_WRITE
     *        TagConstants.MP3_FILE_SAVE_OVERWRITE
     *        TagConstants.MP3_FILE_SAVE_APPEND </code>
     *
     * @throws IOException on any I/O error
     * @throws TagException on any exception generated by this library.
     */
    public void save(File file, int saveMode)
              throws IOException, TagException {
        if ((saveMode < TagConstant.MP3_FILE_SAVE_FIRST) || (saveMode > TagConstant.MP3_FILE_SAVE_LAST)) {
            throw new TagException("Invalid Save Mode");
        }

        RandomAccessFile rfile = null;

        //System.out.println("Saving  : " + file.getAbsolutePath());
        try {
            if (id3v2tag != null) {
                adjustID3v2Padding(TagOptionSingleton.getInstance().getId3v2PaddingSize(), TagOptionSingleton.getInstance().isId3v2PaddingWillShorten(), TagOptionSingleton.getInstance().isId3v2PaddingCopyTag(), file);
            }

            // we can't put these two if's together because
            // adjustid3v2padding needs all handles on the file closed;
            rfile = new RandomAccessFile(file, "rw");

            if (TagOptionSingleton.getInstance().isId3v2Save()) {
                if (id3v2tag == null) {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        (new ID3v2_4()).delete(rfile);
                    }
                } else {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_WRITE) {
                        id3v2tag.write(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_APPEND) {
                        id3v2tag.append(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        id3v2tag.overwrite(rfile);
                    }
                }
            }

            if (TagOptionSingleton.getInstance().isLyrics3Save()) {
                if (lyrics3tag == null) {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        (new Lyrics3v2()).delete(rfile);
                    }
                } else {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_WRITE) {
                        lyrics3tag.write(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_APPEND) {
                        lyrics3tag.append(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        lyrics3tag.overwrite(rfile);
                    }
                }
            }

            if (TagOptionSingleton.getInstance().isId3v1Save()) {
                if (id3v1tag == null) {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        (new ID3v1()).delete(rfile);
                    }
                } else {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_WRITE) {
                        id3v1tag.write(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_APPEND) {
                        id3v1tag.append(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        id3v1tag.overwrite(rfile);

                        int debug = 0; // strange bug where last line is not run??
                    }
                }
            }

            if (TagOptionSingleton.getInstance().isFilenameTagSave()) {
                if (filenameTag != null) {
                    if (saveMode == TagConstant.MP3_FILE_SAVE_WRITE) {
                        filenameTag.write(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_APPEND) {
                        filenameTag.append(rfile);
                    } else if (saveMode == TagConstant.MP3_FILE_SAVE_OVERWRITE) {
                        filenameTag.overwrite(rfile);

                        int debug = 0; // strange bug where last line is not run??
                    }
                }
            }
        } finally {
            if (rfile != null) {
                rfile.close();
            }
        }
    }

    /**
     * Returns true if the first MP3 frame can be found for the MP3 file that
     * this object refers to. This is the first byte of music data and not the
     * ID3 Tag Frame.
     *
     * @return true if the first MP3 frame can be found
     *
     * @throws IOException on any I/O error
     */
    public boolean seekMP3Frame()
                         throws IOException {
        RandomAccessFile rfile = null;
        boolean          found = false;

        try {
            rfile = new RandomAccessFile(this.mp3file, "r");
            found = seekMP3Frame(rfile);
        } finally {
            if (rfile != null) {
                rfile.close();
            }
        }

        return found;
    }

    /**
     * Returns true if the first MP3 frame can be found for the MP3 file
     * argument. It tries to sync as many frame as defined in
     * <code>TagOptions.getNumberMP3SyncFrame</code> This is the first byte of
     * music data and not the ID3 Tag Frame.
     *
     * @param seekFile MP3 file to seek
     *
     * @return true if the first MP3 frame can be found
     *
     * @throws IOException on any I/O error
     */
    public boolean seekMP3Frame(RandomAccessFile seekFile)
                         throws IOException {
        boolean syncFound = false;
        byte    first;
        byte    second;
        long    filePointer = 1;

        variableBitRate = false;

        try {
            seekFile.seek(0);

            do {
                first = seekFile.readByte();

                if (first == (byte) 0xFF) {
                    filePointer = seekFile.getFilePointer();
                    second      = (byte) (seekFile.readByte() & (byte) 0xE0);

                    if (second == (byte) 0xE0) {
                        seekFile.seek(filePointer - 1);

                        // seek the next frames, recursively
                        syncFound = seekNextMP3Frame(seekFile, TagOptionSingleton.getInstance().getNumberMP3SyncFrame());
                    }

                    seekFile.seek(filePointer);
                }
            } while (syncFound == false);

            seekFile.seek(filePointer - 1);
        } catch (EOFException ex) {
            syncFound = false;
        } catch (IOException ex) {
            syncFound = false;
            throw ex;
        }

        return syncFound;
    }

    /**
     * Returns the MP3 frame size for the file this object refers to. It
     * assumes that <code>seekNextMP3Frame</code> has already been called.
     *
     * @return MP3 Frame size in bytes.
     */
    private int getFrameSize() {
        if (this.frequency == 0) {
            return 0;
        }

        int size = 0;
        int paddingByte;

        if (padding) {
            paddingByte = 1;
        } else {
            paddingByte = 0;
        }

        if (this.layer == 3) { // Layer I
            size = (int) ((((12 * this.bitRate) / this.frequency) + paddingByte) * 4);
        } else {
            size = (int) (((144 * this.bitRate) / this.frequency) + paddingByte);
        }

        //if (protection) size += 2;
        return size;
    }

    /**
     * Reads the mp3 frame header from the current posiiton in the file and
     * sets this object's private variables to what is found. It assumes the
     * <code>RandomAccessFile</code> is already pointing to a valid MP3 Frame.
     *
     * @param file File to read frame header
     *
     * @throws IOException on any I/O error
     * @throws TagNotFoundException if MP3 Frame sync bites were not
     *         immediately found
     * @throws InvalidTagException if any of the header values are invlaid
     */
    private void readFrameHeader(RandomAccessFile file)
                          throws IOException, TagNotFoundException, InvalidTagException {
        byte[] buffer = new byte[4];

        file.read(buffer);

        // sync
        if ((buffer[0] != (byte) 0xFF) || ((buffer[1] & (byte) 0xE0) != (byte) 0xE0)) {
            throw new TagNotFoundException("MP3 Frame sync bits not found");
        }

        this.mpegVersion = (byte) ((buffer[1] & TagConstant.MASK_MP3_VERSION) >> 3);
        this.layer       = (byte) ((buffer[1] & TagConstant.MASK_MP3_LAYER) >> 1);
        this.protection  = (buffer[1] & TagConstant.MASK_MP3_PROTECTION) != 1;

        int  bitRateValue = (buffer[2] & TagConstant.MASK_MP3_BITRATE) | (buffer[1] & TagConstant.MASK_MP3_ID) | (buffer[1] & TagConstant.MASK_MP3_LAYER);
        Long object = (Long) TagConstant.bitrate.get(new Long(bitRateValue));

        if (object != null) {
            if (object.longValue() != this.bitRate) {
                this.variableBitRate = true;
            }

            this.bitRate = object.intValue();
        } else {
            throw new InvalidTagException("Invalid bit rate");
        }

        int frequencyValue = (buffer[2] & TagConstant.MASK_MP3_FREQUENCY) >>> 2;

        if (mpegVersion == 3) { // Version 1.0

            switch (frequencyValue) {
                case 0:
                    this.frequency = 44.1;

                    break;

                case 1:
                    this.frequency = 48.0;

                    break;

                case 2:
                    this.frequency = 32.0;

                    break;
            }
        } else if (mpegVersion == 2) { // Version 2.0

            switch (frequencyValue) {
                case 0:
                    this.frequency = 22.05;

                    break;

                case 1:
                    this.frequency = 24.00;

                    break;

                case 2:
                    this.frequency = 16.00;

                    break;
            }
        } else if (mpegVersion == 00) { // Version 2.5

            switch (frequencyValue) {
                case 0:
                    this.frequency = 11.025;

                    break;

                case 1:
                    this.frequency = 12.00;

                    break;

                case 2:
                    this.frequency = 8.00;

                    break;
            }
        } else {
            throw new InvalidTagException("Invalid MPEG version");
        }

        this.padding       = (buffer[2] & TagConstant.MASK_MP3_PADDING) != 0;
        this.privacy       = (buffer[2] & TagConstant.MASK_MP3_PRIVACY) != 0;
        this.mode          = (byte) ((buffer[3] & TagConstant.MASK_MP3_MODE) >> 6);
        this.modeExtension = (byte) ((buffer[3] & TagConstant.MASK_MP3_MODE_EXTENSION) >> 4);
        this.copyProtected = (buffer[3] & TagConstant.MASK_MP3_COPY) != 0;
        this.home          = (buffer[3] & TagConstant.MASK_MP3_HOME) != 0;
        this.emphasis      = (byte) ((buffer[3] & TagConstant.MASK_MP3_EMPHASIS));
    }

    /**
     * Returns true if the first MP3 frame can be found for the MP3 file
     * argument. It is recursive and called by seekMP3Frame. This is the first
     * byte of music data and not the ID3 Tag Frame.
     *
     * @param file MP3 file to seek
     * @param iterations recursive counter
     *
     * @return true if the first MP3 frame can be found
     *
     * @throws IOException on any I/O error
     */
    private boolean seekNextMP3Frame(RandomAccessFile file, int iterations)
                              throws IOException {
        boolean syncFound = false;
        byte[]  buffer;
        byte    first;
        byte    second;
        long    filePointer;

        if (iterations == 0) {
            syncFound = true;
        } else {
            try {
                this.readFrameHeader(file);
            } catch (TagException ex) {
                return false;
            }

            int size = getFrameSize();

            if ((size <= 0) || (size > file.length())) {
                return false;
            }

            buffer = new byte[size - 4];
            file.read(buffer);

            filePointer = file.getFilePointer();
            first       = file.readByte();

            if (first == (byte) 0xFF) {
                second = (byte) (file.readByte() & (byte) 0xE0);

                if (second == (byte) 0xE0) {
                    file.seek(filePointer);

                    // recursively find the next frames
                    syncFound = seekNextMP3Frame(file, iterations - 1);
                } else {
                    syncFound = false;
                }
            } else {
                syncFound = false;
            }
        }

        return syncFound;
    }
}